// 1.0.0 CW 28 Mar 2003 Added HTTPClientHelperEx

Script CHTTPClient
(name, ip, port, timeout, version)

String Name = name;

String Version = version;
String IP = ip;
U2 Port = port;
U4 TimeOut = timeout;		//in ms
I4 RCode;
U0 EID;

CV cvX;		//must acquire this lock before any work (to block all waiting user)
CV cv1;		//must acquire this to write to InString
CV cv2;		//must acquire this to write to OutBody, OutHeader, OutRes;

I4 SockState = 0;		//0 not connected, 1 is connected
Object Sock;

String InString;
String OutHeader, OutBody, OutResponse;

EID = PCall("MessageLoop");

Script Connect
ObjCreateEx(Sock, "QSock");
if (QSO_Socket(Sock, 2, 1, 0))
{
	AppLog(Name, " QSO_Socket failed");
	ObjDelete(Sock);
	return -1;
}

if (QSO_Bind(Sock, "0.0.0.0", 0))
{
	AppLog(Name, " QSO_Bind failed");
	return -1;
}

if (QSO_Connect(Sock, IP, Port))
{
	AppLog(Name, " QSO_Connect failed, ", IP, ":", Port);
	ObjDelete(Sock);
	return -1;
}
AppLog(Name, " connected to ", IP, ":", Port);
SockState = 1;
return 0;
EndScript

Script Disconnect
SockState = 0;
AppLog("QSO_ShutDown ", QSO_ShutDown(Sock, 1));
AppLog("QSO_Close ", QSO_Close(Sock));
ObjDelete(Sock);
AppLog("[Debug] Completed Object Delete");
EndScript


Script SplitHeader
(s, first)
U0 pos;

if (StrFind(s, "\x0D\x0A", 0, pos))
{
	StrSubEx(first, s, 0, pos + 2);
	StrReplace(s, 0, pos + 2, "");
}
EndScript

Script HTTP_SendAndWait
(in, cmd, url, h, outRes, outHeader, outBody)

U4 remain;
I4 ret = -1;
String tmp = in;

CVLock(cvX);

OutBody = "";

CVLock(cv1);
StrAppend(h, "Content-Length: ", StrLen(tmp), "\x0D\x0A\x0D\x0A");
StrAssign(InString, cmd, " ", url, " ", Version, "\x0D\x0A", h, tmp);
AppLog("[", Name, "] ", InString);
ret = -1;
RCode = 0;
CVWakeAll(cv1);
CVUnlock(cv1);

CVLock(cv2);
remain = TimeOut;
Again:
switch (RCode)
{
case 0
	remain = CVWaitEx(cv2, remain);
	if (remain)
	{
		goto Again;
	}
	CVUnlock(cv2);
	break;
	
case 1
	CVUnlock(cv2);
	outRes = "";
	outHeader = OutHeader;
	outBody = OutBody;
	ret = 0;
	break;
	
case -1
	CVUnlock(cv2);
	AppLog("[", Name, "] Failure");
}

CVUnlock(cvX);

return ret;
EndScript

Script CheckSock

String s;
U4 event;

if (QSO_Wait(Sock, 20, 1000, event))		//20 = 16 (remote close) + 4 (read)
{
	if (event & 4)
	{
		AppLog("[", Name, "] detected data during CheckSock");
EmptyAgain:
		if (QSO_Recv(Sock, s, 50000))
		{
			AppLog("[", Name, "] QSO_Recv failure.  Closing socket");
			AppLog("[", Name, "] QSO_Close ", QSO_Close(Sock));
			SockState = 0;
		}
		else
		{
			switch (StrLen(s))
			{
			case 50000
				AppLog("[", Name, "] Discarding unwanted data ", Strlen(s), "bytes");
				goto EmptyAgain;
				
			case 0
				AppLog("[", Name, "] Receive 0 length data.  Closing socket");
				AppLog("[", Name, "] QSO_Close ", QSO_Close(Sock));
				SockState = 0;
				break;
				
			default
				AppLog("[", Name, "] Discarding unwanted data ", Strlen(s), "bytes");
				break;
			}
		}
	}
	
	if (event & 16)
	{
		AppLog("[", Name, "] Remote has shutdown.  Closing socket");
		AppLog("[", Name, "] QSO_Close ", QSO_Close(Sock));
		SockState = 0;
	}
}

EndScript

Script MessageLoop

U0 actual_sent;
QS_SetDescription(Name);

CVLock(cv1);
Again:
if (StrLen(InString))
{
	if (SockState == 0)
	{
		if (Connect())			//if fail to connect
		{
			InString = "";
			CVLock(cv2);
			NotifyRCode(-1);
			goto Again;
		}
	}
	
	//CheckSock();			//an extra step to ensure there is no buffer in the socket
	
	if (QSO_Send(Sock, InString, actual_sent))
	{
		Disconnect();
		InString = "";
		CVLock(cv2);
		NotifyRCode(-1);
		goto Again;
	}
	
	AppLog(Name, " message sent, size = ", StrLen(InString), ", actual = ", actual_sent);
	InString = "";
	HTTP_Receive();
	if (SockState)
	{
		Disconnect();			//an extra step to ensure there is no buffer in the socket
	}
}
CVWait(cv1);
goto Again;

EndScript

Script NotifyRCode
(code)
RCode = code;
CVWakeAll(cv2);
CVUnlock(cv2);
EndScript

Script HTTP_Receive

String s;		//the recv buffer
U4 SplitFlag = 0;
U4 event;
U4 RType = 0;			//0 for not defined, 1 for Content-Length, 2 for chunked, 3 for chunked while parsing body (body length is not yet at the required length)
U4 len;
U0 pos, pos2;
U0 content_length;
U0 PartialChunkOffset = 0;		//PartialChunkOffset is the position at which a PartialChunk starts
String tmp, tmp2;

CVLock(cv2);

XXX:
	if (QSO_Wait(Sock, 20, 5000, event))		//20 = 16 (remote close) + 4 (read)
	{
		AppLog(Name, " error in QSO_Wait");
		Disconnect();
		NotifyRCode(-1);
		return;
	}

	if (event & 1)
	{
		AppLog(Name, " Timeout waiting for response from HTTP server");
		Disconnect();
		NotifyRCode(-1);
		return;
	}
	
	if (event & 2)			//User Event
	{
		AppLog(Name, " unexpected user event");
		Disconnect();
		NotifyRCode(-1);
		return;
	}

	if (event & 4)			//read event
	{
		if (QSO_Recv(Sock, s, 50000))		//the 50000 is a number plug from the air
		{
			AppLog(Name, " QSO_Recv failed");
			Disconnect();
			NotifyRCode(-1);
			return;
		}
		
		if (StrLen(s) == 0)
		{
			AppLog(Name, " received empty data");
			Disconnect();
			NotifyRCode(-1);
			return;
		}

		if (SplitFlag)			//this indicate header is complete
		{
			switch (RType)
			{
			case 1			//Content-Length
				StrAppend(OutBody, s);
				//PrintLn("## ", StrLen(OutBody), "/", content_length);
				if (StrLen(OutBody) == content_length)
				{
					AppLog("Message with content length, ", content_length, " completed");
					NotifyRCode(1);
					return;
				}
				break;
				
			case 2				//chunked
				StrAppend(OutBody, s);
				RType = 2;
Chunked_Again:
				if (StrFind(OutBody, "\x0D\x0A", PartialChunkOffset, pos))
				{
					StrSubEx(tmp, OutBody, PartialChunkOffset, pos);			//tmp now holds the number (in hexadecimal text)
					StrReplace(OutBody, PartialChunkOffset, pos + 2, "");		//remove the number and "\x0D\x0A"
					StrReplace(tmp, 0, 0, StrRepeat(tmp2, "0", 8 - StrLen(tmp)));		//left pad 0 upto 8 characters
					if (StrConvertHex(tmp2, tmp))
					{
						AppLog("[Error] ", ScriptName(), " Unabled to convert hexadecimal number ", tmp, " during chunked processing");
						return;
					}
					BinaryAssign(len, tmp2);
					len = HiLoByte(len);
					if (len == 0)		//found 0 length
					{
						NotifyRCode(1);
						return;
					}
					
					if (StrLen(OutBody) - PartialChunkOffset >= len + 2)
					{
						PartialChunkOffset = PartialChunkOffset + len;
						StrReplace(OutBody, PartialChunkOffset, 2, "");			//remove the "\x0D\x0A"
						goto Chunked_Again;
					}

					RType = 3;
				}
				
				break;
				
				
			case 3					//chunked while parsing
				StrAppend(OutBody, s);
				if (StrLen(OutBody) - PartialChunkOffset >= len + 2)
				{
					PartialChunkOffset = PartialChunkOffset + len;
					RType = 2;
					goto Chunked_Again;
				}
				break;
			}
			
		}
		else		//header is not complete
		{
			if (StrFind(StrAppend(OutBody, s), "\x0D\x0A\x0D\x0A", 0, pos))
			{
				//PrintLn("## Header is complete");
				SplitFlag = 1;
				StrSubEx(OutHeader, OutBody, 0, pos + 2);		//OutHeader ends with \x0D\x0A
				StrReplace(OutBody, 0, pos + 4, "");
				
				if (StrFind(OutHeader, "Content-Length: ", 0, pos) && StrFind(OutHeader, "\x0D\x0A", pos, pos2))		//Look for line with Content-Length 
				{
					RType = 1;
					content_length = StrSub(OutHeader, pos + 16, pos2 - pos - 16);
					if (StrLen(OutBody) == content_length)
					{
						//PrintLn("## Content-Length is ", content_length, " recv is completed");
						NotifyRCode(1);
						return;
					}
					//PrintLn("Content-Length is ", StrLen(s), "/", content_length);
				}
				elseif (StrFind(OutHeader, "Transfer-Encoding: ", 0, pos) && StrFind(OutHeader, "chunked", pos, pos2))		//Look for line with chunked
				{
					RType = 2;
					goto Chunked_Again;					
				}
				else
				{
					AppLog("Length method is not specified");
					NotifyRCode(1);
					return;
				}
			}
		}
	}
	
	if (event & 16)		//close event
	{
		AppLog(Name, " unexpected socket closed at remote");
		Disconnect();
		NotifyRCode(-1);
		return;
	}
	
	//PrintLn("## Before check sock");
	CheckSock();
	
	goto XXX;

EndScript


EndScript


Script HTTPClientHelper
(dst, url, h, msg, resline, resheader, resbody)
String s;
String header;
String resLine, resHeader, resBody;
if (GetActualType(msg) == "String")
{
	s = msg;
}
else
{
	if (JSONWrite(msg, 0, s))
	{
		AppLog("Failed to JSONWrite, ", msg);
		return -1;
	}
}

StrAppend(header, "Host: ", :dst.IP, ":", :dst.Port, "\x0D\x0A");
StrAppend(header, "Accept: */*\x0D\x0A");
StrAppend(header, "Content-Type: application/json\x0D\x0A");
StrAppend(header, h);

AppLog("[", ScriptName(), "] Sending HTTP message to ", dst, "\x0A", s);

if (:dst.HTTP_SendAndWait(s, "POST", url, header, resline, resheader, resbody))
{
	AppLog("[", ScriptName(), "] Failed to perform HTTP_SendAndWait on ", dst);
	return -1;
}
	
AppLog("[", ScriptName(), "] HTTP_SendAndWait is successful.  Response is\x0A", resbody);
	
return 0;
EndScript

Script HTTPClientHelper2
(dst, cmd, url, h, msg, resline, resheader, resbody)
String s, tmp;
String header;
String resLine, resHeader, resBody;
Vector v;
Reference pv;
U0 pos;

if (GetActualType(msg) == "String")
{
	s = msg;
}
else
{
	if (JSONWrite(msg, 0, s))
	{
		AppLog("Failed to JSONWrite, ", msg);
		return -1;
	}
}

StrAppend(header, "Host: ", :dst.IP, ":", :dst.Port, "\x0D\x0A");
StrAppend(header, "Accept: */*\x0D\x0A");
StrAppend(header, "Content-Type: application/json\x0D\x0A");
StrAppend(header, h);

AppLog("[", ScriptName(), "] Sending HTTP message to ", dst, "\x0A", s);

if (:dst.HTTP_SendAndWait(s, cmd, url, header, tmp, resheader, resbody))
{
	AppLog("[", ScriptName(), "] Failed to perform HTTP_SendAndWait on ", dst);
	return -1;
}
	
AppLog("[", ScriptName(), "] HTTP_SendAndWait is successful.  Response is\x0A", resbody);

if (StrFind(resheader, "\x0D\x0A", 0, pos))
{
	switch (GetActualType(resline))
	{
	case "String"
		StrSubEx(resline, resheader, 0, pos);
		StrReplace(resheader, 0, pos + 2, "");
		break;
		
	case "PString"
		StrSubEx(tmp, resheader, 0, pos);
		StrTok(tmp, 0, 0, " ", v);
		foreach(pv, v)
		{
			VectorPush(resline, pv);
		}
		StrReplace(resheader, 0, pos + 2, "");
		break;
	}
	
}
	
return 0;
EndScript


Script HTTPClientHelperEx
(dst, url, h, msg, resline, resheader, resbody)
String s;
String header;
String resLine, resHeader, resBody;
if (GetActualType(msg) == "String")
{
	s = msg;
}
else
{
	if (JSONWrite(msg, 0, s))
	{
		AppLog("Failed to JSONWrite, ", msg);
		return -1;
	}
}

StrAppend(header, "Host: ", dst.IP, ":", dst.Port, "\x0D\x0A");
StrAppend(header, "Accept: */*\x0D\x0A");
StrAppend(header, "Content-Type: application/json\x0D\x0A");
StrAppend(header, h);

AppLog("[", ScriptName(), "] Sending HTTP message to ", dst.Name, "\x0A", s);

if (dst.HTTP_SendAndWait(s, "POST", url, header, resline, resheader, resbody))
{
	AppLog("[", ScriptName(), "] Failed to perform HTTP_SendAndWait on ", dst.Name);
	return -1;
}
	
AppLog("[", ScriptName(), "] HTTP_SendAndWait is successful.  Response is\x0A", resbody);
	
return 0;
EndScript


